using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Globalization;

namespace INdT.Sparta.Graphics
{
    [DataContract]
    public class SpartaDrawable : SpartaObject
    {
        public enum AutoHitTestModes
        {
            Rectangle
            , Ellipse
        }

        #region Vars
        private Vector2 position = Vector2.Zero;
        private Vector2 origin = Vector2.Zero;
        private Vector2 scale = Vector2.One;
        private Color color = Color.White;
        public Color DebugColor = Color.White; //This changes when added on a state
        #endregion

        #region Properties
        public virtual float X
        {
            get { return position.X; }
            set
            {
                if (value != position.X)
                {
                    position.X = value;
                }
            }
        }
        public virtual float Y
        {
            get { return position.Y; }
            set
            {
                if (value != position.Y)
                {
                    position.Y = value;
                }
            }
        }
        [DataMember]
        public virtual Vector2 Position
        {
            get { return position; }
            set
            {
                X = value.X;
                Y = value.Y;
            }
        }

        [DataMember]
        public virtual int Width { get; set; }
        [DataMember]
        public virtual int Height { get; set; }

        public virtual float OriginX
        {
            get { return origin.X; }
            set
            {
                if (value != origin.X)
                {
                    origin.X = value;
                }
            }
        }
        public virtual float OriginY
        {
            get { return origin.Y; }
            set
            {
                if (value != origin.Y)
                {
                    origin.Y = value;
                }
            }
        }
        [DataMember]
        public virtual Vector2 Origin
        {
            get { return origin; }
            set
            {
                OriginX = value.X;
                OriginY = value.Y;
            }
        }

        public virtual float ScaleX
        {
            get { return scale.X; }
            set
            {
                if (value != scale.X)
                {
                    scale.X = value;
                }
            }
        }
        public virtual float ScaleY
        {
            get { return scale.Y; }
            set
            {
                if (value != scale.Y)
                {
                    scale.Y = value;
                }
            }
        }
        [DataMember]
        public Vector2 Scale
        {
            get { return scale; }
            set
            {
                ScaleX = value.X;
                ScaleY = value.Y;
            }
        }

        [DataMember]
        public virtual float Angle { get; set; }

        [DataMember]
        public virtual Color Color
        {
            get { return color; }
            set { color = value; }
        }

        public virtual float Opacity
        {
            get
            {
                return (float)Convert.ToDouble(Color.A, CultureInfo.InvariantCulture) / 255f;
            }
            set
            {
                Color newColor = this.Color;
                newColor.A = Convert.ToByte(255f * MathHelper.Clamp(value, 0f, 1f), CultureInfo.InvariantCulture);
                this.Color = newColor;
            }
        }

        public virtual bool Visible
        {
            get { return Opacity > 0f; }
            set { Opacity = value ? 1f : 0f; }
        }

        [DataMember]
        public virtual AutoHitTestModes AutoHitTestMode { get; set; }

        public virtual Rectangle BoundingBox
        {
            get
            {
                Rectangle boundingBox = new Rectangle((int)X, (int)Y, (int)Math.Ceiling(Width * ScaleX), (int)Math.Ceiling(Height * ScaleY));
                boundingBox.Offset((int)Math.Ceiling(-OriginX * ScaleX), (int)Math.Ceiling(-OriginY * ScaleY));
                return boundingBox;
            }
        }

        public virtual Vector2 BoundingBoxOrigin
        {
            get
            {
                Rectangle boundingBox = this.BoundingBox;
                return new Vector2(boundingBox.X, boundingBox.Y);
            }
        }

        protected Rectangle AjustedBounds
        {
            get
            {
                Rectangle bounds = BoundingBox;
                bounds.X += bounds.Width / 2;
                bounds.Y += bounds.Height / 2;
                return bounds;
            }
        }

        #endregion

        #region Methods
        public SpartaDrawable()
        {
        }

        public virtual void LoadContent()
        {
        }

        public virtual void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {
            if (SpartaGame.Instance.debugDraw)
            {
                Vector2 elementOrigin = Position;
                Vector2 initialBBPosition = new Vector2(BoundingBox.X, BoundingBox.Y);
                Matrix myRotationMatrix = Matrix.CreateRotationZ(Angle);

                // Rotate relative to origin.
                Vector2 rotatedVector = Vector2.Transform(initialBBPosition - elementOrigin, myRotationMatrix);

                // Add origin to get final location.
                initialBBPosition = rotatedVector + elementOrigin;

                spriteBatch.Draw(SpartaGame.Instance.debugPixel, initialBBPosition, new Rectangle(0, 0, BoundingBox.Width, BoundingBox.Height), DebugColor, Angle, Vector2.Zero, Scale, SpriteEffects.None, 0f);

                //Ponto
                spriteBatch.Draw(SpartaGame.Instance.debugPixel, Position, new Rectangle(0, 0, 3, 3), Color.Black, Angle, new Vector2(1.5f), Scale, SpriteEffects.None, 0f);
            }
        }

        public virtual bool CollidesWithDrawable(SpartaDrawable drawable)
        {
            if (drawable == null || this == drawable)
            {
                return false;
            }
            return drawable.BoundingBox.Intersects(this.BoundingBox);
        }

        /// <summary>
        /// Determines if a collision has occurred on an Axis of one of the
        /// planes parallel to the Rectangle
        /// </summary>
        /// <param name="theRectangle"></param>
        /// <param name="aAxis"></param>
        /// <returns></returns>
        private bool IsAxisCollision(SpartaDrawable theRectangle, Vector2 aAxis)
        {
            //Project the corners of the Rectangle we are checking on to the Axis and
            //get a scalar value of that project we can then use for comparison
            List<int> aRectangleAScalars = new List<int>();
            aRectangleAScalars.Add(GenerateScalar(theRectangle.UpperLeftCorner(), aAxis));
            aRectangleAScalars.Add(GenerateScalar(theRectangle.UpperRightCorner(), aAxis));
            aRectangleAScalars.Add(GenerateScalar(theRectangle.LowerLeftCorner(), aAxis));
            aRectangleAScalars.Add(GenerateScalar(theRectangle.LowerRightCorner(), aAxis));

            //Project the corners of the current Rectangle on to the Axis and
            //get a scalar value of that project we can then use for comparison
            List<int> aRectangleBScalars = new List<int>();
            aRectangleBScalars.Add(GenerateScalar(UpperLeftCorner(), aAxis));
            aRectangleBScalars.Add(GenerateScalar(UpperRightCorner(), aAxis));
            aRectangleBScalars.Add(GenerateScalar(LowerLeftCorner(), aAxis));
            aRectangleBScalars.Add(GenerateScalar(LowerRightCorner(), aAxis));

            //Get the Maximum and Minium Scalar values for each of the Rectangles
            int aRectangleAMinimum = aRectangleAScalars.Min();
            int aRectangleAMaximum = aRectangleAScalars.Max();
            int aRectangleBMinimum = aRectangleBScalars.Min();
            int aRectangleBMaximum = aRectangleBScalars.Max();

            GC.SuppressFinalize(aRectangleAScalars);
            GC.SuppressFinalize(aRectangleBScalars);

            //If we have overlaps between the Rectangles (i.e. Min of B is less than Max of A)
            //then we are detecting a collision between the rectangles on this Axis
            if (aRectangleBMinimum <= aRectangleAMaximum && aRectangleBMaximum >= aRectangleAMaximum)
            {
                return true;
            }

            if (aRectangleAMinimum <= aRectangleBMaximum && aRectangleAMaximum >= aRectangleBMaximum)
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Generates a scalar value that can be used to compare where corners of 
        /// a rectangle have been projected onto a particular axis. 
        /// </summary>
        /// <param name="theRectangleCorner"></param>
        /// <param name="theAxis"></param>
        /// <returns></returns>
        private int GenerateScalar(Vector2 theRectangleCorner, Vector2 theAxis)
        {
            //Using the formula for Vector projection. Take the corner being passed in
            //and project it onto the given Axis
            float aNumerator = (theRectangleCorner.X * theAxis.X) + (theRectangleCorner.Y * theAxis.Y);
            float aDenominator = (theAxis.X * theAxis.X) + (theAxis.Y * theAxis.Y);
            float aDivisionResult = aNumerator / aDenominator;
            Vector2 aCornerProjected = new Vector2(aDivisionResult * theAxis.X, aDivisionResult * theAxis.Y);

            //Now that we have our projected Vector, calculate a scalar of that projection
            //that can be used to more easily do comparisons
            float aScalar = (theAxis.X * aCornerProjected.X) + (theAxis.Y * aCornerProjected.Y);
            return (int)aScalar;
        }

        /// <summary>
        /// Rotate a point from a given location and adjust using the Origin we
        /// are rotating around
        /// </summary>
        /// <param name="thePoint"></param>
        /// <param name="theOrigin"></param>
        /// <param name="theRotation"></param>
        /// <returns></returns>
        public Vector2 RotatePoint(Vector2 thePoint, Vector2 theOrigin, float theRotation)
        {
            Vector2 aTranslatedPoint = new Vector2();
            aTranslatedPoint.X = (float)(Math.Cos(theRotation) * (thePoint.X - theOrigin.X) - Math.Sin(theRotation) * (thePoint.Y - theOrigin.Y) + theOrigin.X);
            aTranslatedPoint.Y = (float)(Math.Sin(theRotation) * (thePoint.X - theOrigin.X) + Math.Cos(theRotation) * (thePoint.Y - theOrigin.Y) + theOrigin.Y);
            return aTranslatedPoint;
        }

        public Vector2 UpperLeftCorner()
        {
            Vector2 aUpperLeft = new Vector2(BoundingBox.Left, BoundingBox.Top);
            aUpperLeft = RotatePoint(aUpperLeft, aUpperLeft + BoundingBoxOrigin, Angle);
            return aUpperLeft;
        }

        public Vector2 UpperRightCorner()
        {
            Vector2 aUpperRight = new Vector2(BoundingBox.Right, BoundingBox.Top);
            aUpperRight = RotatePoint(aUpperRight, aUpperRight + new Vector2(-BoundingBoxOrigin.X, BoundingBoxOrigin.Y), Angle);
            return aUpperRight;
        }

        public Vector2 LowerLeftCorner()
        {
            Vector2 aLowerLeft = new Vector2(BoundingBox.Left, BoundingBox.Bottom);
            aLowerLeft = RotatePoint(aLowerLeft, aLowerLeft + new Vector2(BoundingBoxOrigin.X, -BoundingBoxOrigin.Y), Angle);
            return aLowerLeft;
        }

        public Vector2 LowerRightCorner()
        {
            Vector2 aLowerRight = new Vector2(BoundingBox.Right, BoundingBox.Bottom);
            aLowerRight = RotatePoint(aLowerRight, aLowerRight + new Vector2(-BoundingBoxOrigin.X, -BoundingBoxOrigin.Y), Angle);
            return aLowerRight;
        }

        public virtual bool IsPointInObject(Vector2 point)
        {
            switch (AutoHitTestMode)
            {
                case AutoHitTestModes.Rectangle:
                    return IsPointInRectangle(point);
                case AutoHitTestModes.Ellipse:
                    return IsPointInEllipse(point);
                default:
                    return false;
            }
        }

        public bool IsPointInRectangle(Vector2 point)
        {
            Rectangle bbox;
            float width;
            float height;
            Vector2 rotatedPoint = Vector2.Zero;

            // Retrieve the bounding box
            bbox = BoundingBox;

            // If no rotation is applied, we can simply check against the bounding box
            if (Angle == 0) return bbox.Contains((int)point.X, (int)point.Y);

            // Get the sprite width and height
            width = bbox.Width;
            height = bbox.Height;

            // Subtract the sprite position to retrieve the test point in
            // object space rather than in screen space
            point -= Position;

            // Rotate the point by the negative angle of the sprite to cancel out the sprite rotation
            rotatedPoint.X = (float)(Math.Cos(-Angle) * point.X - Math.Sin(-Angle) * point.Y);
            rotatedPoint.Y = (float)(Math.Sin(-Angle) * point.X + Math.Cos(-Angle) * point.Y);

            //System.Diagnostics.Debug.WriteLine(rotatedPoint.ToString());

            // Move the bounding box to object space too
            bbox.Offset((int)-X, (int)-Y);

            // Does the bounding box contain the rotated sprite?
            return bbox.Contains((int)rotatedPoint.X, (int)rotatedPoint.Y);
        }

        protected bool IsPointInEllipse(Vector2 point)
        {
            Rectangle bbox;
            Vector2 rotatedPoint = Vector2.Zero;

            // Retrieve the basic sprite bounding box
            bbox = BoundingBox;

            // Subtract the ellipse's top-left position from the test point so that the test
            // point is relative to the origin position rather than relative to the screen
            point -= Position;

            // Rotate the point by the negative angle of the sprite to cancel out the sprite rotation
            rotatedPoint.X = (float)(Math.Cos(-Angle) * point.X - Math.Sin(-Angle) * point.Y);
            rotatedPoint.Y = (float)(Math.Sin(-Angle) * point.X + Math.Cos(-Angle) * point.Y);

            // Add back the origin point multiplied by the scale.
            // This will put us in the top-left corner of the bounding box.
            rotatedPoint += Origin * Scale;
            // Subtract the bounding box midpoint from each axis.
            // This will put us in the center of the ellipse.
            rotatedPoint -= new Vector2(bbox.Width / 2, bbox.Height / 2);

            // Divide the point by the width and height of the bounding box.
            // This will result in values between -0.5 and +0.5 on each axis for
            // positions within the bounding box. As both axes are then on the same
            // scale we can check the distance from the center point as a circle,
            // without having to worry about elliptical shapes.
            rotatedPoint /= new Vector2(bbox.Width, bbox.Height);

            //System.Diagnostics.Debug.WriteLine(rotatedPoint.Length());

            // See if the distance from the origin to the point is <= 0.5
            // (the radius of a unit-size circle). If so, we are within the ellipse.
            return (rotatedPoint.Length() <= 0.5f);
        }
        #endregion
    }
}
